#!/usr/bin/python2.4
#
# TestResults includes:
#   test_view objects
#   job_host_run objects
#   Test progress stats


import itertools
import operator
import threading
import traceback

import constants
import inquirer
import inspector
import job_host_run as job_host_run_module
from logger import logger
import sql_job_host_run
import test_stats as test_stats_module
import utils


class TestResults(object):

  def __init__(self, test_plan, inspector_path=None):
    self.test_plan = test_plan
    self.inspector_path = inspector_path
    self.inspector_mtime_and_size = None
    self.inspector_globals = None
    self.inspector = None
    self.inquirer = inquirer.Inquirer(test_plan)
    self.lock = threading.RLock()

    # Hosts
    self.all_hosts = None
    self.hosts = None
    self.discarded_hosts = None
    # Results
    self.all_test_views = None
    self.test_views = None
    self.discarded_test_views = None
    self.all_job_host_runs = None
    self.job_host_runs = None
    self.discarded_job_host_runs = None
    # Index
    self.job_tag_to_run = {} # dict{job_tag}=JobHostRun
    self.host_job_results = {} # dict{hostname}{job_name}=[JobHostRun]
    self.host_results = {} # dict{hostname}=[JobHostRun]
    # Stats
    self.stats_hosts = None
    self.overall_stats = None
    self.test_stats = None
    self.host_stats = None
    self.host_job_status = {} # dict{hostname}{job_name}=status
    # Run stats
    self.run_stats_hosts = None
    self.overall_run_stats = None
    self.test_run_stats = None
    self.host_run_stats = None

  def __repr__(self):
    results = '%s(name=%r, %d tests' % (type(self).__name__,
                                        self.test_plan.name,
                                        len(self.test_plan.tests))
    if self.hosts is not None:
      results += ', %d hosts' % len(self.hosts)
    if self.job_host_runs is not None:
      results += ', %d runs' % len(self.job_host_runs)
    results += ')'
    return results

  def __getitem__(self, key):
    return self.job_tag_to_run[key]

  @property
  def hostnames(self):
    return (host.hostname for host in self.hosts)

  def get_duration(self):
    overall_duration = test_stats_module.TotalCount()
    test_duration = dict((test.job_name, test_stats_module.TotalCount())
                         for test in self.test_plan.tests)
    host_duration = dict((hostname, test_stats_module.TotalCount())
                         for hostname in self.hostnames)
    for host in self.hosts:
      host_labels_set = set(host.labels)
      for test in self.test_plan.tests:
        if test.deps_set <= host_labels_set:
          overall_duration.add(test.duration)
          test_duration[test.job_name].add(test.duration)
          host_duration[host.hostname].add(test.duration)
    return overall_duration, test_duration, host_duration

  def setup(self, query_results=True, update_hosts=False):
    self.inquirer.setup()
    if query_results:
      self.get_results(update_hosts=update_hosts)
      self.compute_stats()
      self.compute_run_stats()

  def get_sql_query_conditions(self, *args, **dargs):
    conditions = []
    for name, op, value in utils.parse_filters(**dargs):
      if name == 'job_name' and op is None:
        conditions.append("%s LIKE '%%: %s'" % (name, value))
      else:
        if name == 'test_label':
          name = 'test_labels.name'
        conditions.append(utils.convert_to_sql(name, op, value))
    conditions += args
    return self.test_plan.get_sql_query_conditions(*conditions)

  def get_detailed_test_views(self, **filter_data):
    return self.inquirer.get_detailed_test_views(**filter_data)

  def get_test_views_with_labels(self, **filter_data):
    return self.inquirer.get_test_views_with_labels(**filter_data)

  def get_sql_test_views(self, **filter_data):
    return self.inquirer.get_sql_test_views(**filter_data)

  def get_sql_job_host_runs(self, **filter_data):
    return self.inquirer.get_sql_job_host_runs(**filter_data)

  def create_inspector(self):
    if self.inspector_path:
      mtime_and_size = utils.mtime_and_size(self.inspector_path)
      if mtime_and_size != self.inspector_mtime_and_size:
        self.inspector_mtime_and_size = mtime_and_size
        self.inspector_globals = {'InspectorBase': inspector.InspectorBase}
        try:
          execfile(self.inspector_path, self.inspector_globals)
        except:
          self.inspector_mtime_and_size = None
          logger.error("Failed to exec inspector file '%s'\n%s" % (
              self.inspector_path, traceback.format_exc()))
          return None
      try:
        return self.inspector_globals['Inspector'](self)
      except:
        logger.error("Failed to create inspector from '%s'\n%s" % (
            self.inspector_path, traceback.format_exc()))
    return None

  def get_results(self, op=constants.DEFAULT_GET_RESULTS_METHOD,
                  update_hosts=False, **filter_data):

    def filter_results(function, sequence):
      if not function:
        return sequence, []
      return function(sequence)

    inspector = self.create_inspector()

    if update_hosts:
      all_hosts = self.test_plan.get_hosts(update_hosts=True)
    else:
      all_hosts = self.test_plan.hosts
      for host in all_hosts:
        host.overrides = None

    hosts, discarded_hosts = filter_results(
        getattr(inspector, 'filter_hosts', None),
        all_hosts)

    if op != 'get_sql_job_host_runs':
      if op == 'get_sql_test_views':
        job_host_run_type = sql_job_host_run.JobHostRun
      else:
        job_host_run_type = job_host_run_module.JobHostRun

      all_test_views = getattr(self.inquirer, op)(**filter_data)

      test_views, discarded_test_views = filter_results(
          getattr(inspector, 'filter_test_views', None),
          all_test_views)

      all_job_host_runs = [
          job_host_run_type(list(test_views))
          for job_idx, test_views in itertools.groupby(
              test_views, operator.attrgetter('job_idx'))]
    else:
      job_host_run_type = sql_job_host_run.JobHostRunWithJobTest
      all_test_views = test_views = discarded_test_views = None
      all_job_host_runs = getattr(self.inquirer, op)(**filter_data)

    job_name_to_test = self.test_plan.job_name_to_test
    all_job_host_runs = [job_host_run for job_host_run in all_job_host_runs
                         if job_host_run.test_job_name in job_name_to_test]

    job_tag_to_run = {}
    test_job_idx_idx = {}
    host_job_idx_idx = {}
    for job_host_run in all_job_host_runs:
      hostname = job_host_run.hostname
      job_name = job_host_run.test_job_name
      job_host_run.job_idx_idx = len(job_tag_to_run)
      idx = job_host_run.test_job_idx_idx = test_job_idx_idx.get(
          job_name, 0)
      test_job_idx_idx[job_name] = idx + 1
      idx = job_host_run.host_job_idx_idx = host_job_idx_idx.get(
          hostname, 0)
      host_job_idx_idx[hostname] = idx + 1
      job_tag_to_run[job_host_run.job_tag] = job_host_run

    job_host_runs, discarded_job_host_runs = filter_results(
        getattr(inspector, 'filter_job_host_runs', None),
        all_job_host_runs)

    host_job_results = {}
    host_results = {}
    for job_host_run in job_host_runs:
      hostname = job_host_run.hostname
      job_name = job_host_run.test_job_name
      (host_job_results.setdefault(hostname, {}).
                        setdefault(job_name, []).
                        append(job_host_run))
      host_results.setdefault(hostname, []).append(job_host_run)

    try:
      self.lock.acquire()
      self.inspector = inspector
      self.all_hosts = hosts
      self.hosts = hosts
      self.discarded_hosts = discarded_hosts
      self.all_test_views = all_test_views
      self.test_views = test_views
      self.discarded_test_views = discarded_test_views
      self.all_job_host_runs = all_job_host_runs
      self.job_host_runs = job_host_runs
      self.discarded_job_host_runs = discarded_job_host_runs
      self.job_tag_to_run = job_tag_to_run
      self.host_job_results = host_job_results
      self.host_results = host_results
    finally:
      self.lock.release()

    return job_host_runs

  def log_results(self, level=logger.WARNING):
    results = ['host_job_results =']
    for hostname in self.hostnames:
      results.append(' host = %s' % hostname)
      for test in self.test_plan.tests:
        results.append("  test = '%s'" % test.job_name)
        job_host_runs = self.host_job_results.get(
            hostname, {}).get(test.job_name, [])
        for job_host_run in job_host_runs:
          lines = job_host_run.pformat(include_test_views=True).splitlines()
          for line in lines:
            results.append('   %s' % line)
    logger.logging(level, '\n'.join(results))

    results = ['host_results =']
    for hostname in self.hostnames:
      results.append(' host = %s' % hostname)
      job_host_runs = self.host_results.get(hostname, [])
      for job_host_run in job_host_runs:
        lines = job_host_run.pformat(include_test_views=True).splitlines()
        for line in lines:
          results.append('  %s' % line)
    logger.logging(level, '\n'.join(results))

  def get_host_job_status(self, hostname, job_name):
    # Returns job_host_run.status - ('INVALIDATED', 'RESOLVED') + 'PENDING'
    job_host_runs = self.host_job_results.get(hostname, {}).get(job_name, [])
    for job_host_run in reversed(job_host_runs):
      status = job_host_run.status
      if status not in ('INVALIDATED', 'RESOLVED'):
        return status, job_host_run
    return 'PENDING', None

  def compute_stats(self):
    overall_stats = test_stats_module.TestStats('Overall')
    test_stats = dict(
        (test.job_name, test_stats_module.TestStats(test.job_name))
        for test in self.test_plan.tests)
    host_stats = {}
    host_job_status = {}
    for host in self.hosts:
      host_labels_set = set(host.labels)
      job_status = host_job_status[host.hostname] = {}
      stats = host_stats[host.hostname] = test_stats_module.TestStats(
          host.hostname)
      for test in self.test_plan.tests:
        if test.deps_set <= host_labels_set:
          status, job_host_run = self.get_host_job_status(host.hostname,
                                                          test.job_name)
          overall_stats.add(status, test.duration,
                            run=job_host_run)
          test_stats[test.job_name].add(status, test.duration,
                                        run=job_host_run)
          stats.add(status, test.duration,
                    run=job_host_run)
          job_status[test.job_name] = status, job_host_run
        else:
          job_status[test.job_name] = 'N/A', None

    try:
      self.lock.acquire()
      self.stats_hosts = self.hosts
      self.overall_stats = overall_stats
      self.test_stats = test_stats
      self.host_stats = host_stats
      self.host_job_status = host_job_status
    finally:
      self.lock.release()

  def log_status(self, level=logger.ERROR):
    results = []
    for hostname in self.hostnames:
      results.append('Host %s' % hostname)
      job_status = self.host_job_status.get(hostname, {})
      for test in self.test_plan.tests:
        results.append(' %s: %r' % (
            test.job_name, job_status.get(test.job_name)))
    logger.logging(level, '\n'.join(results))

  def log_stats(self, level=logger.ERROR, include_hosts=True):
    results = [self.overall_stats.pformat()]
    results.append(' tests '.center(80, '-'))
    for test in self.test_plan.tests:
      results.append(self.test_stats[test.job_name].pformat())
    if include_hosts:
      results.append(' hosts '.center(80, '-'))
      for hostname in self.hostnames:
        results.append(self.host_stats[hostname].pformat())
    logger.logging(level, '\n'.join(results))

  def compute_run_stats(self):

    def add_stats_to_dict(stats_dict, key, status, run_time_in_hour,
                          job_host_run):
      stats = stats_dict.get(key)
      if stats is None:
        stats = stats_dict[key] = test_stats_module.TestStats(key)
      stats.add(status, run_time_in_hour,
                run=job_host_run)

    overall_run_stats = test_stats_module.TestStats('Overall Run')
    test_run_stats = {}
    host_run_stats = {}
    for job_host_run in self.job_host_runs:
      status = job_host_run.status
      if status != 'INVALIDATED':
        hostname = job_host_run.hostname
        job_name = job_host_run.test_job_name
        run_time, queued_time = job_host_run.running_time
        run_time_in_hour = run_time / 3600.
        overall_run_stats.add(status, run_time_in_hour,
                              run=job_host_run)
        add_stats_to_dict(test_run_stats, job_name, status, run_time_in_hour,
                          job_host_run)
        add_stats_to_dict(host_run_stats, hostname, status, run_time_in_hour,
                          job_host_run)

    try:
      self.lock.acquire()
      self.run_stats_hosts = self.hosts
      self.overall_run_stats = overall_run_stats
      self.test_run_stats = test_run_stats
      self.host_run_stats = host_run_stats
    finally:
      self.lock.release()

  def log_run_stats(self, level=logger.ERROR, include_hosts=True):
    results = [self.overall_run_stats.pformat()]
    results.append(' tests run '.center(80, '-'))
    for test in self.test_plan.tests:
      stats = self.test_run_stats.get(test.job_name)
      if stats:
        results.append(stats.pformat())
    if include_hosts:
      results.append(' hosts run '.center(80, '-'))
      for hostname in self.hostnames:
        stats = self.host_run_stats.get(hostname)
        if stats:
          results.append(stats.pformat())
    logger.logging(level, '\n'.join(results))
